<?php
/**
 * base class for files
 *
 * This file is part of Webappkit, a packages system for PHP web apps
 * @link http://webappkit.net
 * @package webappkit
 * @subpackage fs
 * @author J.Ducastel <nospam0@ducastel.name>
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 */
class WakFile implements iWakFS {

    /**
     * @param string $path
     */
    public function __construct($path) {
        $this->_path=$path;
    }

    /**
     * return given path
     * @return string
     */
    public function path() {
        return $this->_path;
    }

    /**
     * @return string
     */
    public function realPath() {
        return realpath($this->_path);
    }

    /**
     * get dirname
     * @return string
     */
    public function dirname() {
        return dirname($this->_path);
    }

    /**
     * @return string
     */
    public function basename() {
        return basename($this->_path);
    }

    /**
    * get file size in octets
    * @access public
    * @return int
    */
    public function size() {
        return $this->exists()?filesize($this->realpath()):0;
    }/**/

    /**
    * does the file exists ?
    * @param
    * @access public
    * @return bool
    */
    public function exists() {
        return file_exists($this->realpath());
    }/**/

    /**
    * returns md5 hash of the file's content
    * @param
    * @access public
    * @return
    */
    public function md5() {
        return md5_file($this->realpath());
    }/**/

    /**
    * is the file readable ?
    * @access public
    * @return bool
    */
    public function isReadable() {
        /*if (PHP_OS=='WINNT')
            return true;*/
        //clearstatcache();
        return is_readable($this->realpath());
    }/**/

    /**
    * is file writable ?
    * @param
    * @access public
    * @return
    */
    public function isWritable() {
        /*if (PHP_OS=='WINNT')
            return true;*/
        return is_writable($this->realpath());
    }

    /**
     * has file the expected extension ?
     * @param string $extension
     * @return bool
     */
    public function hasExtension($extension) {
        return substr($this->basename,-strlen($extension))==$extension;
    }

    /**
     * create and return a copy of the file
     * @param string $path
     * @return WakFile copy should have the same class than orginal file
     */
    public function getCopy($path) {
        if (!$this->exists())
            return false;
        if (!copy($this->path(), $path))
            return false;
        $class = get_class($this);
        return new $class($path);
    }

    /**
    * read some bytes.
    * Reads 1Mo if bytes is not provided. From offset, if provided, else current offset.
    * @param int bytes optional 1mo by default
    * @param int offset optional uses current offset by default
    * @access public
    * @return mixed file content
    */
    public function read($bytes=1048576, $offset=null) {
        /*if (!$this->isReadable()) {
            trigger_error('filenode::read : file'.$this->_path.' is not readable',E_USER_NOTICE);
            return false;
        }*/
        if (!$this->exists()) {
            // trigger_error('read() could not read if file dont exists, '.$this->_path);
            return false;
        }
        // getting handle
        if (!$handle=&$this->handle()) {
            trigger_error('filenode::read could not read '.$this->_path);
            return false;
        }
        // setting offset if necessary
        if (is_int($offset))
            $this->offset($offset);
        //echo "<p>offset is ".$this->offset().'</p>';
        return fread($handle,$bytes);
    }/**/

    /**
    * write some data.
    * From offset, if provided, else current offset.
    * @param mixed $content
    * @param int offset optional. uses current offset if omitted
    * @access public
    * @return bool
    */
    public function write($content, $offset=null) {
        // is file writable
        /*if (!$this->isWritable()) {
            trigger_error('filenode::write : file '.$this->path.' is not writable',E_USER_NOTICE);
            return false;
        }*/
        // getting handle
        if (!$handle = &$this->handle()) {
            return false;
        }
        // setting offset if necessary
        if (is_int($offset))
            $this->offset($offset);
        if (!fwrite($handle, $content))
            return false;
        $this->_clearCache();
        return true;
    }/**/

    /**
    * erase from offset
    * @param int offset
    * @return bool
    */
    public function erase($offset=NULL) {
        // getting handle
        if (!$handle=&$this->handle())
            return false;
        // setting offset if necessary
        //$oldOffset=$this->offset();
        $from=$this->offset($offset);
        // echo $from;
        $ok=ftruncate($handle, $from);
        // echo $this->offset();
        // $this->offset($offset);
        return $ok;
    }

    /**
    * returns and edventually sets handle’s offset.
    * @param int offset optional sets new offset
    * @whence int SEEK_CUR|SEEK_END|SEEK_SET
    * @access public
    * @return int current offset
    */
    public function offset($offset=null,$whence=NULL) {
        // getting file handle
        if (!$handle=&$this->handle()) {
            trigger_error("could not get handle to set new offset $offset");
            return false;
        }
        // is new offset provided
        if (is_int($offset)) {
            // setting whence
            switch ($whence) {
                case SEEK_CUR: // new offset is from current position
                case SEEK_END: // new offset is from the end
                    break;
                default: // new offset is from start
                    $whence=SEEK_SET;
            }
            // trying to set new offset
            fseek($handle,$offset,$whence);
        }
        return ftell($handle);
    }/**/

    /**
    * destroy file
    * @access public
    * @return bool
    */
    public function delete() {
        if (!$this->exists()) return false;
        return unlink($this->realpath());
    }

    /**
     * creates file if don't exists
     * @return bool
     */
    public function create() {
        if (!$this->handle('w+'))
            return false;
        $this->close();
        return true;
    }

    /**
    * moves to new folder (keeps basename)
    * @param string $new_path
    * @param string $new_name if null, will keep basename
    * @access public
    * @return bool
    */
    public function move($new_path, $new_name=null) {
        if (!$this->exists())
            return false;
        $old = $this->realPath();
        if (!is_string($new_name))
            $new_name = $this->basename();
        $new = realpath($new_path).$new_name;
        // echo $new; return false;
        if (!rename($old,$new))
            return false;
        $this->_path = $new;
        return true;
    }

    /**
     * renames file (new basename within same folder)
     * @return bool
     */
    public function rename($name) {
        $old=$this->realPath();
        $dir=$this->dirname();
        $new=$dir?($dir.'/'.$name):$name; // echo "<p>$old => $new</p>";
        if (!rename($old,$new))
            return false;
        $this->_path=$new;
        return true;
    }

    /**
    * sets offset to end of file
    * @return bool
    */
    public function toEnd() {
        $off = $this->offset(0, SEEK_END); // echo "<p>offset = $off</p>";
        return true;
    }
    /**
    * sets offset to start of file
    * @return bool
    */
    public function toStart() {
        return ($this->offset(0, SEEK_SET)==0);
    }

    /**
    * returns file handle
    * @access public
    * @param string $mode
    * @return resource file hande
    */
    public function & handle($mode=null) {
        if (!$this->_handle) {
            // defining default mode if required
            if (is_null($mode)) {
                switch (true) {
                    case $this->exists() and $this->isWritable():
                        // read/write, handle at start
                        $mode='r+';
                        break;
                    case $this->exists():
                        // read only, handle at start
                        $mode='r';
                        break;
                    default:
                        // read/write, create file, handle at start
                        $mode='w+';
                }
            }
            // opening in requested mode
            if ($this->_handle=fopen($this->_path,$mode)) {
                $this->_mode=$mode;
            } else
                trigger_error('failed to open '.$this->_path.' in mode '.$mode);
        }
        return $this->_handle;
    }

    /**
     * close file handle
     * @return bool
     */
    public function close() {
        if (!fclose($this->handle()))
            return false;
        $this->_handle=null;
        return true;
    }

    /**
     * @var string file's path
     */
    protected $_path;

    /**
     * @var resource file's handle
     */
    protected $_handle;

    /**
     * @var string handle's mode
     */
    protected $_mode;

    /**
     * @var int handler position, in octets since start
     */
    // protected $_offset=0;

    /**
     * clears files information cache - to be used whenever file changes
     * @return bool
     */
    protected function _clearCache() {
        clearstatcache();
        return true;
    }
}
